This repository has no description
1import { error, redirect } from "@sveltejs/kit";
2import { createBobbinClient } from "$lib/api/client";
3import { count } from "$lib/api/count";
4import { getStarRkey } from "$lib/api/graph";
5import { IdentityCache, resolveMiniDoc } from "$lib/api/identity";
6import { getDefaultBranch } from "$lib/api/knot";
7import { parallel, toHttpError } from "$lib/api/load";
8import { getRepo } from "$lib/api/records";
9import { repoNameOf, resolveRepoByName } from "$lib/api/repo";
10import { didFromUri, rkeyFromUri } from "$lib/api/uri";
11import type { BobbinContext } from "$lib/api/client";
12import type { RepoCounts, RepoInfo, RepoSource } from "$lib/components/repo/types";
13import type { LayoutLoad } from "./$types";
14
15const FALLBACK_BRANCH = "main";
16
17const resolveSource = async (
18 ctx: BobbinContext,
19 uri: string | undefined
20): Promise<RepoSource | null> => {
21 if (!uri?.startsWith("at://")) return null;
22 try {
23 const view = await getRepo(ctx, uri);
24 const ownerDid = didFromUri(view.uri);
25 const owner = await new IdentityCache(ctx).resolve(ownerDid).catch(() => null);
26 return { ownerHandle: owner?.handle ?? ownerDid, name: repoNameOf(view) };
27 } catch {
28 // a fork whose source is gone still renders, just without the attribution
29 return null;
30 }
31};
32
33// the layout reset also cuts us off from `[handle]/+layout.ts`, so identity gets
34// resolved again here
35export const load: LayoutLoad = async (event) => {
36 const parent = await event.parent();
37 const identifier = decodeURIComponent(event.params.handle);
38 const name = decodeURIComponent(event.params.repo);
39
40 // rejects bare words so unrelated paths 404 instead of resolving as actors
41 if (!identifier.startsWith("did:") && !identifier.includes(".")) {
42 error(404, "Not found");
43 }
44
45 const ctx = createBobbinClient({ serviceUrl: parent.publicConfig.bobbinUrl, fetch: event.fetch });
46 const doc = await resolveMiniDoc(ctx, identifier).catch((cause) =>
47 toHttpError(cause, "Could not resolve user")
48 );
49
50 // redirects dids and stale handles to the canonical handle url
51 const canonical = doc.handle && !doc.handle.endsWith(".invalid") ? doc.handle : null;
52 if (canonical && identifier.toLowerCase() !== canonical.toLowerCase()) {
53 redirect(307, `/${canonical}/${event.params.repo}${event.url.search}`);
54 }
55
56 const view = await resolveRepoByName(ctx, doc.did, name).catch((cause) =>
57 toHttpError(cause, "Could not load repository")
58 );
59 if (!view) error(404, `${doc.handle}/${name} does not exist`);
60
61 const record = view.value;
62 const repoDid = record.repoDid;
63 const viewerDid = parent.auth?.did;
64
65 const stats = await parallel({
66 // only the knot knows the default branch. a knot that is down or still
67 // syncing shouldn't take out the whole layout
68 defaultBranch: getDefaultBranch(ctx, { repo: view.uri })
69 .then((branch) => branch.name)
70 .catch(() => null),
71 stars: repoDid
72 ? count(ctx, "sh.tangled.feed.countStars", repoDid).catch(() => null)
73 : Promise.resolve(null),
74 // the tabs count what is still open, closed ones matter on their own pages
75 issues: repoDid
76 ? count(ctx, "sh.tangled.repo.countIssues", repoDid, { state: "open" }).catch(() => null)
77 : Promise.resolve(null),
78 pulls: repoDid
79 ? count(ctx, "sh.tangled.repo.countPulls", repoDid, { status: "open" }).catch(() => null)
80 : Promise.resolve(null),
81 forks: repoDid
82 ? count(ctx, "sh.tangled.repo.countForks", repoDid).catch(() => null)
83 : Promise.resolve(null),
84 viewerStarRkey:
85 viewerDid && repoDid
86 ? getStarRkey(ctx, viewerDid, repoDid).catch(() => null)
87 : Promise.resolve(null),
88 source: resolveSource(ctx, record.source)
89 });
90
91 const repo: RepoInfo = {
92 uri: view.uri,
93 rkey: rkeyFromUri(view.uri),
94 name: repoNameOf(view),
95 ownerDid: doc.did,
96 ownerHandle: doc.handle,
97 repoDid,
98 knot: record.knot,
99 spindle: record.spindle,
100 description: record.description,
101 website: record.website,
102 topics: record.topics,
103 source: stats.source ?? undefined,
104 defaultBranch: stats.defaultBranch ?? FALLBACK_BRANCH
105 };
106
107 const counts: RepoCounts = {
108 stars: stats.stars?.count ?? 0,
109 issues: stats.issues?.count ?? 0,
110 pulls: stats.pulls?.count ?? 0,
111 forks: stats.forks?.count ?? 0
112 };
113
114 return { repo, counts, viewerStarRkey: stats.viewerStarRkey };
115};